"""程序的控制中心"""
import os
import time
import psutil
import shutil
import signal
import subprocess
from src.mod import run
from src.utils import mq
from src.core import log
from src.core import Enums
from src.core import Cache
from src.db import dbm, dbs
from datetime import datetime
from src.mod import Distribute
from src.mod import safe_remove
from psutil import NoSuchProcess
from src.core.variables.temp import *
from src.utils.deep import get_os_info
from src.core.thread_pool import tpoll
from src.core.variables.constant import *
from watchdog.observers import Observer
from src.core.variables.exception import *
from watchdog.events import FileModifiedEvent
from src.core.variables.temp import MIDDLEWARE
from watchdog.events import FileSystemEventHandler


HOSTING_JAVA_REALOG_DICT: dict = {}


class _BBM:
    """back_base_manager, 后端应用基础管理类"""

    def __init__(self) -> None:
        pass

    def load(
        self,
        cpu: bool = False,
        memory: bool = False,
        network: bool = False,
        IO: bool = False,
        full: bool = False,
    ):
        """应用负载"""
        # 使用psutils根据pid获取应用负载信息
        if self.pid is None:
            log.ERROR(f"{self.app_id} 没有正在运行的进程！")
            log.WARNING(f"出发错乱保护，尝试停止应用...")
            self.panic_protect()
            raise ValueError(f"{self.app_id} 没有正在运行的进程！")
        try:
            process = psutil.Process(self.pid)
            if full:
                cpu, memory, network, IO = True, True, True, True
            # 获取CPU使用率
            if cpu:
                cpu_usage = process.cpu_percent(interval=1)
            # 获取内存使用情况
            if memory:
                # 单位自适应
                def auto_units(bytes_num):
                    for unit in ["B", "KB", "MB", "GB", "TB"]:
                        if bytes_num < 1024 or unit == "TB":
                            return f"{bytes_num:.2f} {unit}"
                        bytes_num /= 1024

                memory_usage = auto_units(process.memory_INFO().rss)
            # 获取网络使用情况
            if network:
                # Get network stats for all interfaces and sum them up
                connections = process.connections()
                network_usage = {
                    "connections_count": len(connections),
                    # Note: Per-process network bytes sent/received isn't directly available
                    # You might need to use system-wide monitoring tools for this
                }
            # 获取IO使用情况
            if IO:
                io_counters = process.io_counters()
                io_usage = {
                    "read_count": io_counters.read_count,
                    "write_count": io_counters.write_count,
                    "read_bytes": io_counters.read_bytes,
                    "write_bytes": io_counters.write_bytes,
                }
            return {
                "cpu": cpu_usage if cpu else None,
                "memory": memory_usage if memory else None,
                "network": network_usage if network else None,
                "IO": io_usage if IO else None,
            }
        except psutil.NoSuchProcess:
            log.ERROR(f"{self.app_id} 的进程不存在！")
            return {
                "cpu": None,
                "memory": None,
                "network": None,
                "IO": None,
            }
        except psutil.AccessDenied:
            ...
            # TODO 监测后台进程是否存在，不存在则触发错乱保护，存在着可能是权限不足
            # log.ERROR(f"{self.app_id} 无法访问进程信息，可能是权限不足！")

    def running_time(self):
        """应用运行时间"""
        now_time = int(datetime.now().timestamp() * 1e6)
        if self.time is None:
            log.ERROR(f"{self.app_id} 没有正在运行的进程！")
            log.WARNING(f"出发错乱保护，尝试停止应用...")
            self.panic_protect()
            raise ValueError(f"{self.app_id} 没有正在运行的进程！")
        else:

            def format_timedelta(start_timestamp, end_timestamp):
                seconds = int(end_timestamp - start_timestamp)
                minutes, seconds = divmod(seconds, 60)
                hours, minutes = divmod(minutes, 60)
                days, hours = divmod(hours, 24)
                months, days = divmod(days, 30)  # 近似计算
                years, months = divmod(months, 12)
                parts = []
                if years > 0:
                    parts.append(f"{years}年")
                if months > 0:
                    parts.append(f"{months}个月")
                if days > 0:
                    parts.append(f"{days}天")
                if hours > 0:
                    parts.append(f"{hours}小时")
                if minutes > 0 or not parts:  # 至少显示分钟
                    parts.append(f"{minutes}分钟")

                return " ".join(parts)

            return format_timedelta(self.time / 1e6, now_time / 1e6)

    def restart(self):
        """重启应用"""
        log.INFO(f"重启应用: {self.app_id}")
        self.stop()
        return self.start()

    def listen(self):
        """监听应用状态"""
        t_name = f"{self.app_id}监听器"
        if t_name in tpoll.get_thread_names():
            tpoll.stop_thread(t_name)

        def wapper(upper):
            while upper.listen_status:
                time.sleep(1)
                try:
                    if upper.pid is not None:
                        is_process = False
                        process = psutil.Process(int(upper.pid))
                        for i in process.cmdline():
                            if f"//{upper.app_id}" in i:
                                is_process = True
                        if process.name() == upper.runner and is_process:
                            self.status = Enums.AppStatus.JAVA_STATUS.RUNNING
                            self._update_status()
                    else:
                        # TODO 记录功能完善后，增加error原因查询
                        # match self.status:
                        #     case Enums.AppStatus.JAVA_STATUS.ERROR:
                        #         ...
                        #     case Enums.AppStatus.JAVA_STATUS.RUNNING:
                        self.status = Enums.AppStatus.JAVA_STATUS.DEAD
                        self._update_status()
                except NoSuchProcess as e:
                    log.DEBUG(f"Java应用出现错误<NoSuchProcess>：{e}")
                    upper.stop()
                    upper.status = Enums.AppStatus.JAVA_STATUS.ERROR
                    upper._update_status()
                    continue
                except Exception as e:
                    log.DEBUG(f"Java应用出现错误：{e}")
                    rcr = run(f"ps -ef | grep java | grep {upper.app_id}")
                    log.DEBUG(f"命令反馈：{rcr}")
                    if upper.app_id not in rcr:
                        upper.status = Enums.AppStatus.JAVA_STATUS.DEAD
                        upper._update_status()
                    continue

        tpoll.start_thread(t_name, wapper, self)


class Jar_manager(_BBM):
    """JAR应用管理器，继承自BBM，提供JAR应用的启动、停止、实时日志等功能"""

    def __init__(self, app_id):
        from src.db.library.hosting_Java import App
        super().__init__()
        self.runner = "java"
        self.observer = None
        self.app_id: str = app_id
        # 在恢复器中将orphan对象作为虚拟process进行模拟操作，应当避免使用Popen对象原有属性
        self.process = None
        self.app: App = None
        self._data_init()
        # self.realog_status: bool = False
        self.status: str = Enums.AppStatus.JAVA_STATUS.DEAD
        self.pid: int = None
        # 创建日志路径
        time_dir = get_os_info().local_time("continuous")
        self.log_path = f"{CONF.PATH.LOG}{self.app.alias}/{time_dir}.log"
        self._update_status()
        self.listen_status = True
        self.file_path = (
            f"{CONF.PATH.REPO}{self.app.alias}/{self.app.time_dir}/{self.app.file_name}"
        )
        self.listen()

    def _data_init(self):
        """初始化数据"""
        with dbm.get("hosting_Java") as db_session:
            from sqlalchemy.orm import scoped_session
            db_session: scoped_session
            db_result = (
                db_session.query(dbs.hosting_Java.App)
                .filter(dbs.hosting_Java.App.app_id == self.app_id)
                .first()
            )
            if db_result:
                self.app = db_result
            else:
                raise ValueError(f"{self.app_id} 不存在！")

    def _update_status(self):
        """更新应用状态"""
        with dbm.get("hosting_Java") as db_session:
            from sqlalchemy.orm import scoped_session
            db_session: scoped_session
            db_session.query(dbs.hosting_Java.App).filter(
                dbs.hosting_Java.App.app_id == self.app_id
            ).update({"status": self.status.value})
            if not dbm.commit(db_session):
                raise MAIN_EXCEPTION(f"提交数据库更改失败！")

    def start(self, jvm_args=""):
        """启动JAR进程并捕获日志"""
        match self.status:
            case Enums.AppStatus.JAVA_STATUS.RUNNING:
                log.WARNING(f"{self.app_id} 已经在运行！")
                if self.pid is None:
                    # TODO
                    ...
            case Enums.AppStatus.JAVA_STATUS.DEAD:
                # 分发应用软链接到运行池
                self._link_path = Distribute.repo_to_run(
                    self.app_id, self.app.node, self.app.time_dir
                )
                try:
                    if not os.path.exists(os.path.dirname(self.log_path)):
                        os.makedirs(os.path.dirname(self.log_path), exist_ok=True)
                    if not os.path.exists(self.log_path):
                        open(self.log_path, "w").close()
                except Exception as e:
                    log.ERROR(f"创建日志目录失败: {e}")
                    raise
                self._log_file = open(self.log_path, "a")
                # 合成启动命令并执行
                _cmd = ["nohup", "java", f"-DfmRunTag={self.app.file_name}"] + jvm_args.split() + ["-jar", self._link_path]
                self.process = subprocess.Popen(
                    _cmd,  # 启动命令
                    bufsize=1,  # 行缓冲
                    stdout=self._log_file,  # 日志输出到日志文件
                    stderr=subprocess.STDOUT,  # 错误输出到标准输出
                    universal_newlines=True,  # 启用文本模式
                    start_new_session=True,  # 允许子进程独立于父进程运行
                )
                log.DEBUG(f'{self.app_id} 启动命令: {" ".join(_cmd)}')
                log.DEBUG(f"{self.app_id} 重定向日志输出到: {self.log_path}")
                log.INFO(f"{self.app_id} 启动成功！")
                self.pid = self.process.pid
                self.status = Enums.AppStatus.JAVA_STATUS.RUNNING
                # 保存到数据库
                with dbm.get("hosting_Java") as db_session:
                    from sqlalchemy.orm import scoped_session
                    db_session: scoped_session
                    db_session.query(dbs.hosting_Java.App).filter(
                        dbs.hosting_Java.App.app_id == self.app_id
                    ).update(
                        {
                            "pid": self.process.pid,
                            "run_path": self._link_path,
                            "log_path": self.log_path,
                            "status": self.status.value,
                        }
                    )
                    if not dbm.commit(db_session):
                        self.stop()
                        raise MAIN_EXCEPTION(f"提交数据库更改失败！")
                    # 获取时间戳
                    self.time = int(datetime.now().timestamp() * 1e6)
                    Cache.HOSTING_JAVA_PID_CACHE.set(self.app_id, True)
                    self._update_status()
                    return self.process.pid

    def stop(self):
        """停止JAR进程"""
        try:
            if self.process:
                try:
                    # 等待进程结束
                    self.process.terminate()
                    self.process.wait(timeout=3)
                except subprocess.TimeoutExpired:
                    # 如果进程未结束，则强制杀死
                    self.process.kill()
                    self.process.wait()
                except psutil.TimeoutExpired:
                    # 如果进程未结束，则强制杀死
                    # TIPS 此处psutil.TimeoutExpired异常与subprocess.TimeoutExpired不属于Exception异常类，因此需要单独处理
                    self.process.kill()
                    self.process.wait()
                except Exception as e:
                    log.ERROR(f"停止进程失败: {e}")
                finally:
                    self.status = Enums.AppStatus.JAVA_STATUS.DEAD
                    # 停止实时日志
                    # if hasattr(self, "observer") and self.realog_status:
                    #     self.stop_realog()
                    # self.realog_status = False
                    log.INFO(f"{self.app_id} 已停止！")
                    # 如果是原生popen对象，则进行额外的操作
                    if type(self.process) is subprocess.Popen:
                        self._log_file.close()
                        safe_remove(self._link_path)
                    Cache.HOSTING_JAVA_PID_CACHE.delete(self.app_id)
                    self.process = None
                    del self.process
                    # 状态写入数据库
                    with dbm.get("hosting_Java") as db_session:
                        from sqlalchemy.orm import scoped_session
                        db_session: scoped_session
                        db_session.query(dbs.hosting_Java.App).filter(
                            dbs.hosting_Java.App.app_id == self.app_id
                        ).update({"pid": "", "run_path": "", "status": self.status.value})
                        if not dbm.commit(db_session):
                            raise MAIN_EXCEPTION(f"提交数据库更改失败！")
            elif self.pid:
                os.kill(self.pid, signal.SIGKILL)
                log.INFO(f"{self.app_id} 已被强制杀死！")
            else:
                log.INFO("没有正在运行的进程。")
            self._update_status()
            self.pid: int = None
        except AttributeError as e:
            log.WARNING(f"runner对象不存在: {e}")

    def histlog(self):
        """历史日志"""
        pass

    def panic_protect(self):
        """错乱恐慌保护"""
        # 进程状态与对象属性状态不一致时触发
        num: int = 0
        self.status = Enums.AppStatus.JAVA_STATUS.ERROR
        log.WARNING(f"{self.app_id} 状态不一致，触发错乱恐慌保护机制，正在重启进程！")
        while self.status is not Enums.AppStatus.JAVA_STATUS.RUNNING or num == 3:
            num += 1
            match self.status:
                case Enums.AppStatus.JAVA_STATUS.ERROR:
                    log.WARNING(f"{self.app_id} 重启失败！")
                case Enums.AppStatus.JAVA_STATUS.RUNNING:
                    log.INFO(f"{self.app_id} 重启成功！")
                case Enums.AppStatus.JAVA_STATUS.DEAD:
                    log.WARNING(f"{self.app_id} 未启动，正在重新尝试！")
                    self.start()
                case Enums.AppStatus.JAVA_STATUS.CRITICAL:
                    log.ERROR(f"{self.app_id} 状态异常：{self.status}")
                    self._update_status()
                    break
            continue
        log.INFO(f"{self.app_id}状态恢复正常，错乱恐慌保护机制已结束！")

    def remove(self):
        """删除应用"""
        # 如果应用正在运行，先停止应用
        if self.status == Enums.AppStatus.JAVA_STATUS.RUNNING:
            self.stop()
        # 删除日志文件
        log_dir = os.path.dirname(self.log_path)
        if os.path.exists(log_dir):
            safe_remove(log_dir, is_dir=True)
        # 删除仓库文件
        repo_path = f"{CONF.PATH.REPO}{self.app.alias}"
        safe_remove(repo_path, is_dir=True)
        # 删除运行池链接
        safe_remove(self.app.run_path)
        log.INFO(f"{self.app_id} 已删除！")
        self._update_status()
        self.listen_status = False

    def __exit__(self):
        self.listen_status = False


class _FBM:
    """front_base_manager, 前端应用基础管理类"""

    def __init__(self, app_id) -> None:
        self.nginx: Nginx = MIDDLEWARE.NGINX
        self.app_id = app_id

    def listen(self):
        """监听应用状态"""
        t_name = f"{self.app_id}监听器"
        if t_name in tpoll.get_thread_names():
            tpoll.stop_thread(t_name)

        def wapper(upper: _FBM):
            ...

        tpoll.start_thread(t_name, wapper, self)


class Static_manager(_FBM):
    def __init__(self, app_id) -> None:
        from src.db.library.hosting_Static import App
        super().__init__(app_id)
        self.runner = 'nginx'
        self.app_id: str = app_id
        self.app: App = None
        self.include_status = False
        self.status: str = Enums.AppStatus.STATIC_STATUS.UNMAPPED
        self.conf_path = f'{CONF.PATH.ABS}/data/software/fm_nginx/conf/fm_conf/{app_id}.conf'
        self.listen_status = True
        self._data_init()
        self.listen()
    
    def _data_init(self, ):
        with dbm.get("hosting_Static") as db_session:
            from sqlalchemy.orm import scoped_session
            db_session: scoped_session
            db_result = db_session.query(dbs.hosting_Static.App).filter(dbs.hosting_Static.App.app_id == self.app_id).first()
            if db_result:
                self.app = db_result
                self.app.run_path = f'{CONF.PATH.RUN_STATIC}/{self.app.alias}'
            else:
                raise ValueError(f"应用{self.app_id}不存在！")
            self.include_status = self.nginx.get_include_status(self.conf_path)
            if self.include_status:
                self.status = Enums.AppStatus.STATIC_STATUS.MAPPTING
            else:
                self.status = Enums.AppStatus.STATIC_STATUS.UNMAPPED
                
    def enable(self):
        """启用"""
        log.INFO(f'包解压...')
        zip_path = f'{CONF.PATH.REPO}/{self.app.alias}/{self.app.time_dir}/{self.app.file_name}'
        MIDDLEWARE.UNZIP(zip_path, f'{CONF.PATH.RUN_STATIC}/{self.app.alias}')
        # 校验第一个index.html在哪一层，避免因为多层目录导致无法404
        log.INFO(f'生成nginx配置...')
        directory = os.path.dirname(self.conf_path)
        if directory and not os.path.exists(directory):
            os.makedirs(directory, exist_ok=True)
        self.__dir_optimize(self.app.run_path)
        with open(self.conf_path, 'w') as f:
            conf = self.app.nginx_conf.replace('@运行池', f"{self.app.run_path}")
            f.write(conf)
        log.INFO(f'include配置到nginx.conf')
        self.nginx.change_include(self.conf_path, 'add')
        log.INFO(f'执行nginx的reload操作')
        self.nginx.reload()
        self.status = Enums.AppStatus.STATIC_STATUS.MAPPTING
        with dbm.get("hosting_Static") as db_session:
            db_session.query(dbs.hosting_Static.App).filter(dbs.hosting_Static.App.app_id == self.app_id).update(
                {dbs.hosting_Static.App.status: self.status.value}
            )
            if dbm.commit(db_session):
                log.INFO(f'应用{self.app_id}启用成功！')
            else:
                log.INFO(f'应用{self.app_id}启用失败！')

    def disable(self, ):
        """停用"""
        # 删除运行池中的静态资源
        safe_remove(self.app.run_path, is_dir=True)
        # 停用nginx配置
        self.nginx.change_include(self.conf_path, 'remove')
        safe_remove(self.conf_path)
        safe_remove(self.app.run_path)
        self.nginx.reload()
        self.status = Enums.AppStatus.STATIC_STATUS.UNMAPPED
        with dbm.get("hosting_Static") as db_session:
            db_session.query(dbs.hosting_Static.App).filter(dbs.hosting_Static.App.app_id == self.app_id).update(
                {dbs.hosting_Static.App.status: self.status.value}
            )
            if dbm.commit(db_session):
                log.INFO(f'应用{self.app_id}停用成功！')
            else:
                log.INFO(f'应用{self.app_id}停用失败！')

    def remove(self, ):
        """删除"""
        self.disable()
        safe_remove(f'{CONF.PATH.REPO}/{self.app_id}', is_dir=True)
        safe_remove(f'{self.conf_path}')
    
    def panic_protect(self, ):
        """错乱恐慌保护"""
        # TODO 完善panic_protect
        log.WARNING(f'应用{self.app_id}触发错乱恐慌保护！')
        self.nginx.change_include(self.conf_path, 'cancel')
        self.nginx.reload()
        self.status = Enums.AppStatus.STATIC_STATUS.CANCEL
        with dbm.get("hosting_Static") as db_session:
            db_session.query(dbs.hosting_Static.App).filter(dbs.hosting_Static.App.app_id == self.app_id).update(
                {dbs.hosting_Static.App.status: self.status.value}
            )
            if dbm.commit(db_session):
                log.INFO(f'应用{self.app_id}错乱恐慌保护成功！')

    def __exit__(self):
        self.listen_status = False

    def __dir_optimize(self, source_dir: str) -> str:
        """
        递归查找index.html并将其同级目录的所有内容移动到源目录根层级
        
        :param source_dir: 源目录路径
        :return: 移动后的index.html路径 (未找到时返回空字符串)
        """
        source_dir = os.path.abspath(source_dir)
        if not os.path.isdir(source_dir):
            log.ERROR(f"路径不存在或不是目录: {source_dir}")
            return ""
        
        # 递归查找index.html
        found_path = ""
        index_dir = ""
        for root, _, files in os.walk(source_dir):
            if "index.html" in files:
                found_path = os.path.join(root, "index.html")
                index_dir = root  # 保存index.html所在的目录
                break
        
        if not found_path:
            log.ERROR(f"在 {source_dir} 中未找到index.html")
            return ""
        
        # 如果index.html已经在根目录，直接返回
        if os.path.dirname(found_path) == source_dir:
            log.INFO(f"index.html已经在根目录: {found_path}")
            return found_path
        
        log.INFO(f"找到index.html在: {found_path}")
        log.INFO(f"开始移动目录内容: {index_dir} -> {source_dir}")
        
        try:
            # 移动index.html同级目录的所有内容到源目录根层级
            moved_items = []
            for item in os.listdir(index_dir):
                item_path = os.path.join(index_dir, item)
                target_path = os.path.join(source_dir, item)
                
                # 跳过当前目录和父目录的引用
                if item in ('.', '..'):
                    continue
                
                # 如果目标路径已存在，先删除（避免冲突）
                if os.path.exists(target_path):
                    if os.path.isdir(target_path):
                        shutil.rmtree(target_path)
                    else:
                        os.remove(target_path)
                
                # 移动文件或目录
                shutil.move(item_path, target_path)
                moved_items.append(item)
            
            log.INFO(f"成功移动 {len(moved_items)} 个项目: {', '.join(moved_items)}")
            
            # 清理空目录（从内到外清理）
            current_dir = index_dir
            while current_dir != source_dir and current_dir != os.path.dirname(source_dir):
                try:
                    if not os.listdir(current_dir):  # 如果目录为空
                        os.rmdir(current_dir)
                        log.DEBUG(f"清理空目录: {current_dir}")
                    else:
                        log.DEBUG(f"目录非空，保留: {current_dir}")
                        break
                except OSError as e:
                    log.warning(f"清理目录失败 {current_dir}: {e}")
                    break
                current_dir = os.path.dirname(current_dir)
            
            # 返回移动后的index.html路径
            new_index_path = os.path.join(source_dir, "index.html")
            log.INFO(f"目录优化完成，index.html现在位于: {new_index_path}")
            return new_index_path
            
        except Exception as e:
            log.ERROR(f"移动过程中出错: {str(e)}")
            return ""